Skip to content

perf: Eliminate hot path allocations using volatile snapshot + ArrayPool#87

Merged
rian-be merged 1 commit into
mainfrom
develop
Jul 27, 2026
Merged

perf: Eliminate hot path allocations using volatile snapshot + ArrayPool#87
rian-be merged 1 commit into
mainfrom
develop

Conversation

@rian-be

@rian-be rian-be commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Problem Statement

The mutation execution pipeline executes on every state transition. Three redundant per call allocations dominated latency:

  • GetSnapshot(): lock(_lock) + _interceptors.ToArray() - copies entire list even when unchanged
  • GetApplicable(): always allocates List<T> + .ToArray(), even if all interceptors pass filter
  • Array allocation: each call allocates new arrays regardless of filtering requirements

Solution Overview

1. Volatile Snapshot Cache with Lazy Rebuild

private volatile IMutationInterceptor[]? _snapshotCache = Array.Empty<IMutationInterceptor>();
  • volatile ensures readers observe fully published arrays without torn references
  • Lazy rebuild on first read after registration changes
  • Zero lock acquisition on hot path reads are lock free

2. ArrayPool Based Filtering

var buffer = ArrayPool<IMutationInterceptor>.Shared.Rent(snapshot.Length);
  • Eliminates List<T> allocation entirely
  • Reuses pooled buffers for filtered results
  • Returns snapshot directly when no interceptors are filtered

3. ExecutionId Optimization

  • Replaced Guid.NewGuid().ToString() with Interlocked.Increment hex counter
  • Result: 8 character hex string instead of 36 character GUID

Performance Impact

Benchmark Results
Benchmark Before After Delta
NoInterceptor_Baseline 2.161 us / 3.12 KB 1.601 us / 3.00 KB -25.9%
PassiveInterceptor_Enabled 2.252 us / 3.30 KB 1.661 us / 3.00 KB -26.2%
Commit_Performance_NoPolicy 4.349 us 4.027 us -7.4%
Commit_Strict_WithPolicy 5.878 us 5.277 us -10.2%
Optimized Path Analysis
Path Before After
0 interceptors Lock + allocation Returns cached empty array
N interceptors, 0 excluded Lock + List + ToArray() Returns snapshot directly
N interceptors, K excluded Lock + List + ToArray() ArrayPool + new T[count]

Key Design Decisions

Why volatile?

CPU memory barrier on read near free on modern CPUs. Eliminates lock acquisition entirely on hot path. Ensures cross thread visibility without lock.

Why lazy rebuild?

Startup overhead is negligible registration is one time. No rebuild cost if pipeline is never used. First read pays lock cost, subsequent reads are free.

Why ArrayPool?

Eliminates List<T> allocation overhead. Reduces GC pressure from temporary buffers. Buffer reuse for filtered interceptor collections.

Why not ReaderWriterLockSlim?

Writes Register/Unregister happen at startup. Reads happen billions of times at runtime. Volatile visibility + lazy rebuild outperforms any lock-based approach.


Files Changed

InterceptorPipeline.cs

  • Added volatile IMutationInterceptor[]? _snapshotCache
  • Implemented lazy rebuild in GetSnapshot() with double checked locking
  • Replaced List<T> with ArrayPool<T> in GetApplicable()
  • Added [MethodImpl(MethodImplOptions.AggressiveInlining)] for hot path methods
  • Updated XML documentation

MutationEngine.cs

  • Replaced Guid.NewGuid().ToString() with Interlocked.Increment(ref _executionCounter).ToString("x8")
  • Added private static long _executionCounter

Migration

Caution

ExecutionId format change: If consuming executionId in logs, traces, or external systems, the format changes from GUID to hex counter.

Aspect Before After
Format 550e8400-e29b-41d4-a716-446655440000 0000002a
Length 36 characters 8 characters
Uniqueness Global Process-local

For distributed tracing, use CorrelationId (still GUID) instead of executionId.

@github-actions github-actions Bot added the performance Performance improvements or regressions label Jul 27, 2026
@github-actions github-actions Bot added the runtime Runtime implementation and execution flow label Jul 27, 2026
@rian-be rian-be changed the title perf: Eliminate hot-path allocations using volatile snapshot + ArrayPool perf: Eliminate hot path allocations using volatile snapshot + ArrayPool Jul 27, 2026
@rian-be
rian-be merged commit a8c361f into main Jul 27, 2026
40 checks passed
@rian-be rian-be linked an issue Jul 27, 2026 that may be closed by this pull request
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance Performance improvements or regressions runtime Runtime implementation and execution flow

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Perf]: Optimize hot path allocations in mutation engine pipeline

1 participant